package com.ruoyi.file.util

import com.ruoyi.file.properties.AliOssConfigProperties
import cn.hutool.core.io.FileUtil
import com.aliyun.oss.OSS
import com.aliyun.oss.model.CompleteMultipartUploadRequest
import com.aliyun.oss.model.DownloadFileRequest
import com.aliyun.oss.model.InitiateMultipartUploadRequest
import com.aliyun.oss.model.UploadPartRequest
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.stereotype.Service
import java.io.File
import java.io.FileInputStream
import java.io.InputStream


// 每个分片的大小，用于计算文件有多少个分片。单位为字节。
const val PART_SIZE = 1 * 1024 * 1024L;   //1 MB。

@Service
@EnableConfigurationProperties(AliOssConfigProperties::class)
class FileUploadHandler {

    private val log = LoggerFactory.getLogger(FileUploadHandler::class.java)

    @Autowired
    private lateinit var ossClient: OSS

    @Autowired
    private lateinit var aliOssConfigProperties: AliOssConfigProperties

    /**
     * 在下载OSS大文件（超过5 GB）到本地的过程中，如果出现网络中断、程序异常退出等问题导致文件下载失败，甚至重试多次仍无法完成下载，您需要使用断点续传下载的方式。
     * 断点续传下载将需要下载的大文件分成多个较小的分片并发下载，
     * 加速下载完成时间。如果下载过程中，某一分片下载失败，再次下载时会从Checkpoint文件记录的断点继续下载，无需重新下载所有分片。下载完成后，所有分片将合并成完整的文件。
     */
    fun download(url: String, traceId: String): String = try {
        //存放链路id
        MDC.put("trace_uuid", traceId)

        log.info("当前待处理的url是: {}", url)

        ossClient.downloadFile(DownloadFileRequest(aliOssConfigProperties.bucketName, url.objectName())
            .apply {
                downloadFile = url.downLoadFile()
                partSize = PART_SIZE
                taskNum = 10
                isEnableCheckpoint = true
                checkpointFile = url.checkPointFile()
            }).also { log.info("开始执行下载任务: 当前任务执行成功") }
        url.outPath()
    } catch (e: Exception) {
        throw e
    }


    // 如果需要在完成分片上传的同时设置文件访问权限，请参考以下示例代码。
    // completeMultipartUploadRequest.setObjectACL(CannedAccessControlList.Private);
    // 指定是否列举当前UploadId已上传的所有Part。仅在Java SDK为3.14.0及以上版本时，支持通过服务端List分片数据来合并完整文件时，将CompleteMultipartUploadRequest中的partETags设置为null。
    // Map<String, String> headers = new HashMap<String, String>();
    // 如果指定了x-oss-complete-all:yes，则OSS会列举当前UploadId已上传的所有Part，然后按照PartNumber的序号排序并执行CompleteMultipartUpload操作。
    // 如果指定了x-oss-complete-all:yes，则不允许继续指定body，否则报错。
    // headers.put("x-oss-complete-all","yes");
    // completeMultipartUploadRequest.setHeaders(headers);
    fun upload(file: File, traceId: String): String = try {
        //存放链路id
        MDC.put("trace_uuid", traceId)

        //上传id 唯一性
        val uploadId = file.uploadId()

        // 创建CompleteMultipartUploadRequest对象。
        // 在执行完成分片上传操作时，需要提供所有有效的partETags。OSS收到提交的partETags后，会逐一验证每个分片的有效性。当所有的数据分片验证通过后，OSS将把这些分片组合成一个完整的文件。
        val completeMultipartUploadRequest =
            CompleteMultipartUploadRequest(
                aliOssConfigProperties.bucketName,
                file.objectName(),
                uploadId,
                run {
                    file.partCount().let { partCount ->

                        (0 until partCount).map { i ->
                            val startPos = i * PART_SIZE
                            val curPartSize = if ((i + 1 == partCount)) (file.length() - startPos) else PART_SIZE
                            val uploadPartRequest = UploadPartRequest()
                            uploadPartRequest.bucketName = aliOssConfigProperties.bucketName
                            uploadPartRequest.key = file.objectName()
                            uploadPartRequest.uploadId = uploadId
                            // 设置上传的分片流。
                            // 以本地文件为例说明如何创建FIleInputStream，并通过InputStream.skip()方法跳过指定数据。
                            val stream: InputStream = FileInputStream(file)
                            stream.skip(startPos)
                            uploadPartRequest.inputStream = stream
                            // 设置分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
                            uploadPartRequest.partSize = curPartSize
                            // 设置分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出此范围，OSS将返回InvalidArgument错误码。
                            uploadPartRequest.partNumber = i + 1
                            // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
                            log.info("开始上传第${uploadPartRequest.partNumber}个分片")
                            val uploadPartResult = ossClient.uploadPart(uploadPartRequest)
                            // 每次上传分片之后，OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
                            uploadPartResult.partETag
                        }.toList()
                    }
                })

        // 完成分片上传。
        ossClient.completeMultipartUpload(completeMultipartUploadRequest).location
            .also { log.info("完成分片上传任务") }
    } finally {
        FileUtil.del(file)
    }

    fun File.uploadId(): String = ossClient.initiateMultipartUpload(
        InitiateMultipartUploadRequest(
            aliOssConfigProperties.bucketName,
            objectName()
        )
    ).uploadId.also { log.info("获取上传id: $it") }


    fun File.partCount(): Int {
        // 根据上传的数据大小计算分片数。以本地文件为例，说明如何通过File.length()获取上传数据的大小。
        val fileLength = length()
        var partCount = (fileLength / PART_SIZE).toInt()
        if (fileLength % PART_SIZE != 0L) {
            partCount++
        }
        return partCount.also { log.info("获取分片数量 $it") }
    }

    fun String.objectName(): String =
        "face_program/${FileUtil.getName(this)}".also { log.info("开始执行下载任务: 获取当前文件的objectName->$it") }

    fun String.downLoadFile(): String =
        "${aliOssConfigProperties.tempPath}/${FileUtil.getName(this)}".also { log.info("开始执行下载任务： 获取当前文件的下载目录->$it") }

    fun String.checkPointFile(): String =
        "${downLoadFile()}.dcp".also { log.info("开始执行下载任务： 获取当前文件的切片目录->$it") }

    fun String.outPath(): String =
        "${aliOssConfigProperties.tempPath}/${FileUtil.getName(this)}"

    fun File.objectName(): String = "face_program/$name"
}